Skip to content

Fix multipart body-map cap bypass and charset handling in RESTEasy - #12085

Draft
jandro996 wants to merge 7 commits into
masterfrom
alejandro.gonzalez/APPSEC-69139-resteasy-bound-multipart
Draft

Fix multipart body-map cap bypass and charset handling in RESTEasy#12085
jandro996 wants to merge 7 commits into
masterfrom
alejandro.gonzalez/APPSEC-69139-resteasy-bound-multipart

Conversation

@jandro996

@jandro996 jandro996 commented Jul 28, 2026

Copy link
Copy Markdown
Member

What Does This Do

  • Adds MultipartHelper.collectBodyMap(MultipartFormDataInput), called from MultipartFormDataReaderInstrumentation.ReadFromAdvice.after() to build the server.request.body AppSec address for RESTEasy 3.0 multipart requests.
  • Caps the map by total accumulated values (total >= MAX_FILES_TO_INSPECT) rather than by distinct field name, since getFormDataMap() already groups parts by field name — a per-key cap would be trivially bypassed by repeating the same field name on every part.
  • Filters collectBodyMap to text/plain parts only, matching the Jersey reference (jersey3/MultiPartHelper.collectBodyPart): file parts are reported separately via collectFilenames/collectFilesContent and must not also consume the body-map budget. A part with no Content-Type header defaults to text/plain, per InputPart's own javadoc.
  • Defaults undeclared-charset body-map text values to UTF-8 (contentTypeWithDefaultUtf8), matching Jersey's own getValue() behavior, instead of falling back to MultipartContentDecoder's JVM-platform-charset default — which is still used as-is for collectFilesContent() (binary file content), left unchanged.
  • Widens the per-part read/header-extraction exception handling in readContent/contentTypeOf from catch (IOException) to catch (Exception) with log.debug, so a single malformed part (e.g. no matching MessageBodyReader, or an unexpected InputPart implementation) can't abort body/filename/content collection for the rest of the request.
  • Adds 7 new Spock tests in MultipartHelperTest.groovy covering: unchecked-exception resilience, getHeaders() throwing/returning null, UTF-8 charset defaulting, the text/plain filter excluding file parts from the cap, and dummy file parts not exhausting the cap before a real text field.

Motivation

APPSEC-69139: extend the RESTEasy 3.0 multipart AppSec instrumentation to inspect multipart form-field values (server.request.body) with the same bounded-cap semantics already used for file content, replicating the pattern established by the Jersey 3 reference implementation.

Additional Notes

None.

Contributor Checklist

Jira ticket: APPSEC-69139

Note: Once your PR is ready to merge, add it to the merge queue by commenting /merge. /merge -c cancels the queue request. /merge -f --reason "reason" skips all merge queue checks; please use this judiciously, as some checks do not run at the PR-level. For more information, see this doc.

…n handling

Cap collectBodyMap() by total accumulated values (not distinct field
names) to close a bypass where a repeated field name could exhaust the
limit. Filter to text/plain parts only and default undeclared charsets
to UTF-8 for body-map values, matching the Jersey reference. Widen
per-part read/header exceptions to Exception with debug logging so one
malformed part doesn't abort body/filename/content collection for the
whole request.
@jandro996 jandro996 added type: bug fix Bug fix inst: jax-rs JAX-RS instrumentation comp: asm waf Application Security Management (WAF) labels Jul 28, 2026
@jandro996

Copy link
Copy Markdown
Member Author

@codex review

@chatgpt-codex-connector

Copy link
Copy Markdown

Codex Review: Didn't find any major issues. You're on a roll.

Reviewed commit: 6b93c566fe

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

@datadog-official

This comment has been minimized.

@dd-octo-sts

dd-octo-sts Bot commented Jul 28, 2026

Copy link
Copy Markdown
Contributor

🟢 Java Benchmark SLOs — All performance SLOs passed

Suite Status
Startup 🟢 pass

SLO thresholds are defined here based on automatically generated metrics. A warning is raised when results are within 5% of the threshold.

PR vs. master results
Scenario Candidate master Δ (95% CI of mean)
startup:insecure-bank:iast:Agent 13.99 s 14.04 s [-1.1%; +0.4%] (no difference)
startup:insecure-bank:tracing:Agent 13.05 s 13.03 s [-0.7%; +1.1%] (no difference)
startup:petclinic:appsec:Agent 16.79 s 16.69 s [-0.3%; +1.6%] (no difference)
startup:petclinic:iast:Agent 16.84 s 16.88 s [-0.9%; +0.5%] (no difference)
startup:petclinic:profiling:Agent 16.54 s 16.87 s [-3.3%; -0.7%] (maybe better)
startup:petclinic:sca:Agent 16.28 s 16.78 s [-7.5%; +1.5%] (no difference)
startup:petclinic:tracing:Agent 16.04 s 16.09 s [-1.3%; +0.7%] (no difference)

Commit: 46aace94 · CI Pipeline · Benchmarking Platform UI


Load and DaCapo benchmarks can be triggered manually in the GitLab pipeline. Results will appear in the Benchmarking Platform UI after completion.

@jandro996
jandro996 marked this pull request as ready for review July 28, 2026 07:57
@jandro996
jandro996 requested a review from a team as a code owner July 28, 2026 07:57
@jandro996
jandro996 requested review from ygree and removed request for a team July 28, 2026 07:57

@datadog-official datadog-official Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Datadog Autotest: PASS

More details

The multipart changes preserve the intended bounded, text-only collection behavior across repeated fields, mixed file/text parts, missing headers, charset defaults, and body-reader failures. Full module tests could not run because the wrapper required an unavailable network download and the installed Gradle could not satisfy this checkout's Java 25 toolchain.

Was this helpful? React 👍 or 👎

📊 Validated against 1 scenarios · Open Bits AI session

🤖 Datadog Autotest · Commit 6b93c56 · What is Autotest? · @DataDog review to ask questions · Any feedback? Reach out in #autotest

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 6b93c566fe

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

A part declaring Content-Type: text/plain but also carrying a
filename attribute was still treated as a form field by
collectBodyMap(), so an attacker could pad the body-map cap with
disposable text/plain file parts and push a genuine field out of
server.request.body. Exclude any part with a filename attribute
(present, even empty) regardless of its declared media type, matching
the file/field distinction collectFilesContent() already uses.

Addresses a P1 finding from the Codex review on PR #12085.
@jandro996

Copy link
Copy Markdown
Member Author

@codex review

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 310e07ea2d

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

@jandro996
jandro996 marked this pull request as draft July 28, 2026 12:30
- hasFilenameParam() detects the extended filename* Content-Disposition
  parameter as well as plain filename, closing the same cap-padding
  bypass via the extended form (Codex P1 follow-up on PR #12085)
- add regression test for a part carrying only filename* (no plain
  filename)
…resteasy-bound-multipart' into alejandro.gonzalez/APPSEC-69139-resteasy-bound-multipart
@jandro996

Copy link
Copy Markdown
Member Author

@codex review

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 6348c70d57

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

hasFilename() now excludes filename*-only parts from the body map, but
collectFilesContent() still gated on rawFilenameFromContentDisposition(),
which is filename*-blind — such a part reached neither AppSec callback.
Switch the gate to hasFilenameParam() so content inspection still covers
it (Codex P1 follow-up on PR #12085).
@jandro996

Copy link
Copy Markdown
Member Author

@codex review

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: afbe420f61

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

…entDecoder

RFC 7230 optional whitespace (OWS) allows both space and horizontal tab.
extractCharset() only accepted space, so "type;\tcharset=X" was treated as
no-charset-declared, causing MultipartHelper to silently override the
client's declared charset with UTF-8.
@jandro996

Copy link
Copy Markdown
Member Author

@codex review

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 46aace9411

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +93 to +95
return MultipartContentDecoder.extractCharset(contentType) == null
? (contentType == null ? "charset=UTF-8" : contentType + "; charset=UTF-8")
: contentType;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Honor explicit charsets with whitespace around the equals sign

When a valid MIME part uses optional whitespace around the parameter separator, such as Content-Type: text/plain; charset = ISO-8859-1, extractCharset cannot find the literal charset= token. This method consequently appends charset=UTF-8, so collectBodyMap decodes the field using UTF-8 rather than its declared charset; non-ASCII attack payloads can therefore be corrupted before reaching server.request.body and evade AppSec matching. Parse the parameter while allowing MIME whitespace around = before deciding that the charset is undeclared.

Useful? React with 👍 / 👎.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

comp: asm waf Application Security Management (WAF) inst: jax-rs JAX-RS instrumentation type: bug fix Bug fix

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant